
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">

  <title>Generate above and below color visualization in 3D - 4.4</title>

  <link rel="stylesheet" href="https://js.arcgis.com/4.4/esri/css/main.css">
  <link rel="stylesheet" href="https://js.arcgis.com/4.4/dijit/themes/claro/claro.css">
  <script src="https://js.arcgis.com/4.4/"></script>

  <style>
    html,
    body,
    #viewDiv {
      padding: 0;
      margin: 0;
      height: 100%;
      width: 100%;
    }

    #containerDiv {
      background-color: white;
      padding: 3px;
      text-align: center;
    }

    #title {
      font-size: 14pt;
      font-weight: 500;
    }

    .widget-background{
      background-color: white;
      font-size: 12pt;
      padding: 8px;
    }
  </style>

  <script>
    require([
      "esri/Map",
      "esri/views/SceneView",
      "esri/layers/FeatureLayer",
      "esri/widgets/Legend",
      "esri/widgets/Expand",
      "esri/widgets/BasemapGallery",
      "esri/renderers/smartMapping/creators/color",
      "esri/renderers/smartMapping/creators/size",
      "esri/renderers/smartMapping/creators/location",
      "esri/renderers/smartMapping/statistics/histogram",
      "esri/widgets/ColorSlider",
      "esri/widgets/SizeSlider",
      "esri/core/lang",
      "dojo/domReady!"
    ], function(
      Map, SceneView, FeatureLayer, Legend, Expand, BasemapGallery, colorRendererCreator, sizeRendererCreator, locationRendererCreator,
      histogram,
      ColorSlider, SizeSlider, lang
    ) {

      var colorFieldSelect = document.getElementById("color-field-select");
      var sizeFieldSelect = document.getElementById("size-field-select");
      var colorNormCheck = document.getElementById("normalize-color-check");
      var sizeNormCheck = document.getElementById("normalize-size-check");
      var themeOptions = document.getElementById("theme-options");
      var colorSlider = null;
      var sizeSlider = null;
      var defaultSymbol = null;
      var sizeSlideEvent, colorSlideEvent;

      var map = new Map({
        basemap: "streets"
      });

      // The minSize and maxSize of volumetric symbols are determined
      // based on the view/camera the data will be displayed in.

      var view = new SceneView({
        container: "viewDiv",
        map: map,
        camera: {
          position: {
            x: -10669492,
            y: 4511468,
            z: 5786863,
            spatialReference: { wkid: 3857 }
          },
          heading: 0,
          tilt: 0
        }
      });

      // Create a BasemapGallery widget instance and set
      // its container to a div element

      var basemapGallery = new BasemapGallery({
        view: view,
        container: document.createElement("div")
      });

      // Create an Expand instance and set the content
      // property to the DOM node of the basemap gallery widget
      // Use an Esri icon font to represent the content inside
      // of the Expand widget

      var bgExpand = new Expand({
        view: view,
        content: basemapGallery.container,
        expandIconClass: "esri-icon-basemap"
      });

      view.ui.add(bgExpand, "top-left");

      // Create an Expand instance and set the content
      // property to the DOM node of the size slider widget

      var sizeSliderExpand = new Expand({
        view: view,
        content: document.getElementById("size-container"),
        expandIconClass: "esri-icon-pie-chart"
      });
      view.ui.add(sizeSliderExpand, "top-left");

      // Create an Expand instance and set the content
      // property to the DOM node of the color slider widget

      var colorSliderExpand = new Expand({
        view: view,
        content: document.getElementById("color-container"),
        expandIconClass: "esri-icon-environment-settings"
      });
      view.ui.add(colorSliderExpand, "top-left");

      sizeSliderExpand.watch("expanded", function(expanded){
        if(expanded && (colorSliderExpand.expanded || bgExpand.expanded)){
          colorSliderExpand.collapse();
          bgExpand.collapse();
        }
      });

      colorSliderExpand.watch("expanded", function(expanded){
        if(expanded && (sizeSliderExpand.expanded || bgExpand.expanded)){
          sizeSliderExpand.collapse();
          bgExpand.collapse();
        }
      });

      bgExpand.watch("expanded", function(expanded){
        if(expanded && (sizeSliderExpand.expanded || colorSliderExpand.expanded)){
          sizeSliderExpand.collapse();
          colorSliderExpand.collapse();
        }
      });

      // Create FeatureLayer instance with popupTemplate

      var layer = new FeatureLayer({
        url: "https://services.arcgis.com/V6ZHFr6zdgNZuVG0/ArcGIS/rest/services/city_marriage_points/FeatureServer/0",
        outFields: ["*"],
        title: "U.S. cities"
      });

      map.add(layer);

      view.then(function(){

        layer.then(function(){
          view.goTo(layer.fullExtent);
        });

        var legend = new Legend({
          view: view
        });
        view.ui.add(legend, "bottom-right");
        return generateRenderer({
          view: view,
          layer: layer,
          basemap: map.basemap
        });

      });

      themeOptions.addEventListener("change", function(evt){

        var options = {
          layer: layer,
          basemap: map.basemap,
          field: colorFieldSelect.value,
          view: view,
          theme: evt.target.value
        };

        getColorVisualVariable(options);
      });

      basemapGallery.watch("activeBasemap", function(newVal){
        var visualVariables = layer.renderer.visualVariables;

        var hasColorVV = visualVariables && visualVariables.filter(function(vv){
          return vv.type === "color";
        }).length > 0;

        var hasSizeVV = visualVariables && visualVariables.filter(function(vv){
          return vv.type === "size";
        }).length > 0;

        var options = {
          layer: layer,
          basemap: newVal,
          view: view,
          field: colorFieldSelect.value,
          theme: themeOptions.value
        };

        if (hasColorVV){
          getColorVisualVariable(options);
        } else if (hasSizeVV){
          options.sizeVVs = visualVariables.filter(function(vv){
            return vv.type === "size";
          });
          generateRenderer(options);
        } else {
          generateRenderer(options);
        }

      });

      colorFieldSelect.addEventListener("change", function(evt){

        var options = {
          layer: map.layers.getItemAt(0),
          basemap: map.basemap,
          field: evt.target.value,
          view: view,
          theme: themeOptions.value
        };

        getColorVisualVariable(options);
      });

      sizeFieldSelect.addEventListener("change", function(evt){
        var options = {
          layer: layer,
          field: evt.target.value,
          basemap: map.basemap,
          view: view
        };

        getSizeVisualVariables(options);
      });

      colorNormCheck.addEventListener("click", function(evt){
        var options = {
          layer: layer,
          basemap: map.basemap,
          field: colorFieldSelect.value,
          view: view,
          theme: themeOptions.value
        };

        getColorVisualVariable(options);
      });

      sizeNormCheck.addEventListener("click", function(evt){
        var options = {
          layer: layer,
          field: sizeFieldSelect.value,
          basemap: map.basemap,
          view: view
        };

        getSizeVisualVariables(options);
      });

      function getSizeVisualVariables(params){

        var sizeParams = {
          layer: params.layer ? params.layer : layer,
          field: params.sizeField ? params.sizeField : sizeFieldSelect.value,
          normalizationField: sizeNormCheck.checked ? "EDUCBASECY" : null,
          basemap: params.basemap ? params.basemap : null,
          view: view,
          worldScale: true,
          axis: "height"
        };

        if(!sizeParams.field &&
           sizeParams.layer.renderer.visualVariables &&
           SizeSlider
          ){
          var renderer = sizeParams.layer.renderer.clone();
          renderer.visualVariables = renderer.visualVariables.filter(function(vv){
            return vv.type !== "size";
          });
          sizeParams.layer.renderer = renderer;
//           destroy size slider widget
          sizeSlider.destroy();
          sizeSlideEvent.remove();
          sizeSlider = null;
          return;
        }


        var sizeVVs;
        var sizeResponse;

        return sizeRendererCreator.createVisualVariables(sizeParams)
          .then(function(response){
            sizeResponse = response;
            var renderer = sizeParams.layer.renderer.clone();
            var unchangedVV;

            if (renderer.visualVariables){
              unchangedVV = renderer.visualVariables.filter(function(vv){
                return vv.type === "color";
              });
            } else {
              unchangedVV = [];
            }

            sizeVVs = lang.clone(response.visualVariables);
            renderer.visualVariables = sizeVVs.concat(unchangedVV);
            sizeParams.layer.renderer = renderer;

            return histogram({
              layer: sizeParams.layer,
              field: sizeParams.field,
              normalizationField: sizeParams.normalizationField,
              numBins: 30
            });
          }).then(function(sizeHistogram){

            var sizeSliderParams = {};

            sizeSliderParams.statistics = sizeResponse.statistics;
            sizeSliderParams.maxValue = sizeResponse.statistics.max;
            sizeSliderParams.minValue = sizeResponse.statistics.min;
            sizeSliderParams.histogram = sizeHistogram;

            var heightVV = sizeVVs.filter(function(vv){
              return vv.axis === "height";
            })[0];
            sizeSliderParams.visualVariable = heightVV;


            if(!sizeSlider){

              var sizeSliderParent = document.getElementById("size-container");
              var sizeSliderContainer = document.createElement("div");
              sizeSliderParent.appendChild(sizeSliderContainer);
              sizeSliderParams.container = sizeSliderContainer;

              sizeSlider = new SizeSlider(sizeSliderParams);

              // when the user slides the handle(s), update the renderer
              // with the updated size visual variable objects

              sizeSlideEvent = sizeSlider.on("handle-value-change", function() {

                var renderer = layer.renderer.clone();
                var visualVariables = lang.clone(renderer.visualVariables);
                var unchangedVVs = [];
                if(visualVariables){
                  unchangedVVs = visualVariables.filter(function(vv){
                    return vv.axis !== "height";
                  });
                }
                renderer.visualVariables = unchangedVVs.concat(lang.clone(sizeSlider.visualVariable));

                layer.renderer = renderer;
              });

            } else {
              sizeSlider.set(sizeSliderParams);
            }

          }).otherwise(function(error){
          console.error("There was an error with generating the size vv: ", error);
        });

      }

      function getColorVisualVariable(params) {

        var colorParams = {
          layer: params.layer ? params.layer : layer,
          basemap: params.basemap ? params.basemap : map.basemap,
          field: params.colorField ? params.colorField : colorFieldSelect.value,
          normalizationField: colorNormCheck.checked ? "ACSMARBASE" : null,
          view: params.view,
          worldScale: true,
          theme: params.theme ? params.theme : themeOptions.value
        };

        if(!colorParams.field &&
           colorParams.layer.renderer.visualVariables &&
           colorSlider
          ){
          var renderer = colorParams.layer.renderer.clone();
          renderer.visualVariables = renderer.visualVariables.filter(function(vv){
            return vv.type !== "color";
          });
          colorParams.layer.renderer = renderer;
          // destroy color slider widget
          colorSlider.destroy();
          colorSlideEvent.remove();
          colorSlider = null;
          return;
        }

        var colorVV;
        var colorResponse;

        return colorRendererCreator.createVisualVariable(colorParams)
          .then(function(response) {
            colorResponse = response;

            colorVV = lang.clone(response.visualVariable);
            var renderer = colorParams.layer.renderer.clone();

            if(renderer.visualVariables){
              var unchangedVVs = renderer.visualVariables.filter(function(vv){
                return vv.type !== "color";
              });
              renderer.visualVariables = unchangedVVs.concat([colorVV]);
            } else {
              renderer.visualVariables = [colorVV];
            }

            layer.renderer = renderer;

            return histogram({
              layer: colorParams.layer,
              field: colorParams.field,
              normalizationField: colorParams.normalizationField,
              numBins: 30
            });
          }).then(function(colorHistogram) {
            var colorSliderParams = {};

            colorSliderParams.statistics = colorResponse.statistics;
            colorSliderParams.maxValue = colorResponse.statistics.max;
            colorSliderParams.minValue = colorResponse.statistics.min;
            colorSliderParams.histogram = colorHistogram;
            colorSliderParams.visualVariable = colorVV;
            colorSliderParams.numHandles = getNumHandles(colorParams.theme);
            colorSliderParams.syncedHandles = getNumHandles(colorParams.theme) > 2;

            // input the slider parameters in the slider's constructor
            // and add it to the view's UI

            if (!colorSlider) {

              var colorSliderParent = document.getElementById("color-container");
              var colorSliderContainer = document.createElement("div");
              colorSliderParent.appendChild(colorSliderContainer);
              colorSliderParams.container = colorSliderContainer;

              colorSlider = new ColorSlider(colorSliderParams);

              // when the user slides the handle(s), update the renderer
              // with the updated color visual variable object

              colorSlideEvent = colorSlider.on("handle-value-change", function() {
                var renderer = layer.renderer.clone();

                var visualVariables = lang.clone(renderer.visualVariables);
                layer.renderer.visualVariables = [];

                if(visualVariables){
                  var unchangedVVs = visualVariables.filter(function(vv){
                    return vv.type !== "color";
                  });
                  renderer.visualVariables = unchangedVVs.concat([lang.clone(colorSlider.visualVariable)]);
                } else {
                  renderer.visualVariables = [lang.clone(colorSlider.visualVariable)];
                }

                layer.renderer = renderer;
              });

            } else {
              colorSlider.set(colorSliderParams);
            }

          }).otherwise(function(error){
          console.error("There was an error with color vv generator: ", error);
        });

      }

      function generateRenderer(params) {

        var locationParams = {
          layer: params.layer,
          basemap: params.basemap,
          view: params.view,
          symbolType: "3d-volumetric"
        };

        return locationRendererCreator.createRenderer(locationParams)
          .then(function(response){
          var renderer = response.renderer;
          if (params.sizeVVs){
            renderer.visualVariables = params.sizeVVs;
          }
          locationParams.layer.renderer = renderer;
          defaultSymbol = response.renderer.symbol.clone();
          return defaultSymbol;
        }).otherwise(function(err) {
          console.log("there was an error: ", err);
        });
      }

      function getNumHandles(theme){
        return theme === "high-to-low" ? 2 : 3;
      }

    });
  </script>
</head>

<body class="claro">
  <div id="viewDiv"></div>
  <div id="color-container" class="widget-background">
    <h3>Color</h3>
    Field: <select id="color-field-select">
      <option value="" selected></option>
      <option value="ACSNEVMARR">Never Married</option>
      <option value="ACSMARRIED">Married</option>
      <option value="ACSWIDOWED">Widowed</option>
      <option value="ACSDIVORCD">Divorced</option>
    </select>
    Normalize? <input type="checkbox" id="normalize-color-check" checked><br>
    Theme: <select id="theme-options">
      <option value="high-to-low" selected>High to low</option>
      <option value="centered-on">Centered on</option>
      <option value="extremes">Extremes</option>
      <option value="above-and-below">Above and below</option>
    </select><br>
  </div>
  <div id="size-container" class="widget-background">
    <h3>Size</h3>
    Field: <select id="size-field-select">
      <option value="" selected></option>
      <option value="nocollegedegree">No college degree</option>
      <option value="SMCOLL_CY">Some college</option>
      <option value="ASSCDEG_CY">Associate degree</option>
      <option value="BACHDEG_CY">Bachelor degree</option>
      <option value="GRADDEG_CY">Graduate degree</option>
    </select>
    Normalize? <input type="checkbox" id="normalize-size-check" checked><br>
  </div>
</body>

</html>